﻿using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Printing;
using System.Windows.Forms;
using System.Linq;

namespace SuperListEx
{
  public class PrintTable
  {
    #region helper classes
    private class PrintColumn
    {
      public ColumnItemValueAccessor value;
      public ColumnItemValueAccessorWithParameter value2;
      public object o;
      public string name;
      public int width;
      public object valueParameter;
    }

    private class PrintSettingsCache
    {
      public int FontOffset; //the height of the printed font
      public int LeftMargin; //the location of the left margin
      public int RightMargin; //the location of the right margin
      public int TopMargin; //location of the top margin
      public int paperwidth; //the width of the paper
      public int PageNumber; //currnt page number
      public int currentPartialPage; //when printing tables, which part of the table is being worked
      public int NumberOfPageParts; //when printing tables, how many parts there are to the table
      public bool PrintOnlyOne = true; //flag
      public int CurrentXOffset = 0;
      public int PrintableHeight;
      public int MarginBoundWidth;
      public int BottomMargin;
      public int PageRightLocation;
      public int PageTop;
      public int BottomMarginLocation;
      public int workItem = 0;
    }
    #endregion

    #region vars
    private PrintSettingsCache _printSettings;
    private PrintDocument printDocument;
    private static PageSettings pageSettings;
    private static PrinterSettings printerSettings;

    Font printFont;

    private Dictionary<PrintColumn, int> PageColumns; //used when printing columns   
    private List<object> inProgress; //used when printing columns
    public List<object> _toPrint;
    ListControl printfrom;
    #endregion

    #region properties
    /// <summary>
    /// Header image if appropriate
    /// </summary>
    public Image HeaderImage
    {
      get;
      set;
    }

    /// <summary>
    /// The transaction to print
    /// </summary>
    public List<object> ToPrint
    {
      get;
      set;
    }
    #endregion

    #region Constructors
    /// <summary>
    /// Default constructor
    /// </summary>
    public PrintTable(ListControl control)
      : this()
    {
      printfrom = control;
      _toPrint = null;

      printDocument = new PrintDocument();
      printDocument.PrinterSettings = printerSettings;
      printDocument.DefaultPageSettings = pageSettings;
      printDocument.PrintPage += new PrintPageEventHandler(printDocument_PrintPage);
    }

    private PrintTable()
    {
      if (pageSettings == null)
      {
        pageSettings = new PageSettings();
        pageSettings.Margins.Top = 100;
        pageSettings.Margins.Bottom = 100;
        pageSettings.Margins.Left = 100;
        pageSettings.Margins.Right = 100;
      }

      if (printerSettings == null)
      {
        printerSettings = new PrinterSettings();
      }
    }
    #endregion

    #region Setttings
    /// <summary>
    /// Will display the print settings dialog box
    /// </summary>
    public static void showPageSettings()
    {
      new PrintTable(); //init object

      PageSetupDialog pageSetupDialog = new PageSetupDialog();
      pageSetupDialog.PageSettings = pageSettings;
      pageSetupDialog.PrinterSettings = printerSettings;
      pageSetupDialog.AllowOrientation = true;
      pageSetupDialog.AllowMargins = true;
      if (pageSetupDialog.ShowDialog() == DialogResult.OK)
      {
        pageSettings = pageSetupDialog.PageSettings;
        printerSettings = pageSetupDialog.PrinterSettings;
      }
    }
    #endregion

    #region print methods
    /// <summary>
    /// Prints the transaction
    /// </summary>
    public void Print()
    {
      if (ToPrint.Count < 1) return;
      PrintDialog dialog = new PrintDialog();
      dialog.UseEXDialog = true;

      printDocument.DefaultPageSettings = pageSettings;
      dialog.Document = printDocument;
      printFont = new Font("Times New Roman", 12, FontStyle.Regular);

      if (dialog.ShowDialog(printfrom.ParentForm) == DialogResult.OK)
      {
        printDocument.Print();
      }
    }

    /// <summary>
    /// Shows the print preview dialog
    /// </summary>
    public void PrintPreview()
    {
      if (ToPrint.Count < 1) return;
      PrintPreviewDialog dialog = new PrintPreviewDialog();

      printDocument.DefaultPageSettings = pageSettings;
      dialog.Document = printDocument;
      printFont = new Font("Times New Roman", 12, FontStyle.Regular);

      dialog.ShowIcon = false;
      dialog.Document = printDocument;
      dialog.ShowDialog(printfrom.ParentForm);
    }
    #endregion

    #region print events
    /// <summary>
    /// Event handler for printing
    /// </summary>
    void printDocument_PrintPage(object sender, PrintPageEventArgs e)
    {
      if (_printSettings == null)
      {//init print settings
        if (_toPrint == null)
          QueuePrintLines();
        _printSettings = new PrintSettingsCache()
        {
          FontOffset = (int)printFont.GetHeight(e.Graphics),
          LeftMargin = e.MarginBounds.Left,
          RightMargin = e.MarginBounds.Right,
          TopMargin = e.MarginBounds.Top,
          paperwidth = e.PageSettings.PaperSize.Width,
          PageNumber = 0,
          currentPartialPage = 1,
          PrintOnlyOne = ToPrint.Count == 1,
          PrintableHeight = -1,
          MarginBoundWidth = e.MarginBounds.Width,
          BottomMargin = e.PageSettings.Margins.Bottom,
          PageRightLocation = e.PageBounds.Right,
          PageTop = e.PageBounds.Top,
          BottomMarginLocation = e.MarginBounds.Bottom
        };
        if (PageColumns != null)
          _printSettings.NumberOfPageParts = (int)(from s in PageColumns orderby s.Value descending select s.Value).Take(1).First();
      }


      AddPageHeader(sender, e);
      addPageNumber(sender, e);
      PrintMany(sender, e);

      if (!e.HasMorePages)//clear cache
      {
        _printSettings = null;
        _toPrint = null;
      }
    }

    /// <summary>
    /// Print routine for a list of items
    /// </summary>
    void PrintMany(object sender, PrintPageEventArgs e)
    {
      int y = AddColumnHeaders(sender, e, _printSettings.FontOffset);
      if (_printSettings.PrintableHeight == -1)
      {//first run need to set
        _printSettings.PrintableHeight = (int)e.PageSettings.PrintableArea.Height - y - e.PageSettings.Margins.Bottom;
      }

      int allocatedHeight = 0;

      if (_toPrint.Count == 0 && inProgress.Count == 0) //see if work is needed
      {
        e.HasMorePages = false;
        return;
      }

      var currPage = from C
                     in PageColumns
                     where C.Value == _printSettings.currentPartialPage
                     select C;

      while (allocatedHeight < _printSettings.PrintableHeight)
      {
        object currWork = GetWorkItem();
        if (currWork == null) break; //nothing to do

        foreach (var v in currPage)
        {
          object value;
          if (v.Key.value != null)
          {
            value = v.Key.value(currWork);
          }

          else
          {
            value = v.Key.value2(currWork, v.Key.valueParameter);
          }

          //draw vertical line to left side of text
          e.Graphics.DrawLine(Pens.Black, _printSettings.LeftMargin + _printSettings.CurrentXOffset, y, _printSettings.LeftMargin + _printSettings.CurrentXOffset, y + _printSettings.FontOffset);

          //print value
          if (value is string)
            e.Graphics.DrawString(value.ToString(), printFont, Brushes.Black, new RectangleF(_printSettings.LeftMargin + _printSettings.CurrentXOffset, y, v.Key.width, _printSettings.FontOffset));
          else if (value is Image)
            e.Graphics.DrawImage((Image)value, new RectangleF(_printSettings.LeftMargin + _printSettings.CurrentXOffset, y, v.Key.width, _printSettings.CurrentXOffset));

          //omit left line as it will be drawn by next iteration
          _printSettings.CurrentXOffset += v.Key.width;
        }

        //close box, first last vertical line
        e.Graphics.DrawLine(Pens.Black, _printSettings.LeftMargin + _printSettings.CurrentXOffset, y, _printSettings.LeftMargin + _printSettings.CurrentXOffset, y + _printSettings.FontOffset);
        //draw the line that goes across the bottom
        e.Graphics.DrawLine(Pens.Black, _printSettings.LeftMargin, y + _printSettings.FontOffset, _printSettings.LeftMargin + _printSettings.CurrentXOffset, y + _printSettings.FontOffset);

        allocatedHeight += _printSettings.FontOffset;
        y += _printSettings.FontOffset;
        AddItemExtension(e, currWork, ref y, ref allocatedHeight);
        _printSettings.CurrentXOffset = 0;
      }

      if (_printSettings.currentPartialPage < _printSettings.NumberOfPageParts || _toPrint.Count > 0)
      {
        e.HasMorePages = true;

        if (_printSettings.currentPartialPage == _printSettings.NumberOfPageParts)
        {
          _printSettings.currentPartialPage = 1; //reset counter
          _printSettings.workItem = 0;
          inProgress.Clear();
        }
        else
        {
          _printSettings.currentPartialPage++;
          _printSettings.workItem = 0;
        }
      }
      else
        e.HasMorePages = false;
    }

    /// <summary>
    /// If a line item has extended text, adds it below the item
    /// </summary>
    /// <param name="e">Print graphics</param>
    /// <param name="currWork">The item being worked</param>
    /// <param name="currY">Current Y Offset</param>
    /// <param name="AllocatedHeight">Current used height</param>
    private void AddItemExtension(PrintPageEventArgs e, object currWork, ref int currY, ref int AllocatedHeight)
    {
      //handle preview i.e. transaction note
      if (printfrom.SectionFactory is MySectionFactory)
      {
        MySectionFactory factory = printfrom.SectionFactory as MySectionFactory;
        string text = factory.getText(currWork).ToString();
        string[] toPrint;

        if (text != "")
        {
          if (text.Contains(Environment.NewLine))
            toPrint = text.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
          else
            toPrint = new string[] { text };

          foreach (string s in toPrint)
          {
            //draw vertical line to left side of text
            e.Graphics.DrawLine(Pens.Black, _printSettings.LeftMargin, currY, _printSettings.LeftMargin, currY + _printSettings.FontOffset);
            //print text
            if (_printSettings.currentPartialPage == 1)
            {//only print text on first page
              e.Graphics.DrawString(text, printFont, Brushes.Black, new RectangleF(_printSettings.LeftMargin, currY, _printSettings.MarginBoundWidth, _printSettings.FontOffset));
            }
            //close box, first last vertical line
            e.Graphics.DrawLine(Pens.Black, _printSettings.LeftMargin + _printSettings.CurrentXOffset, currY, _printSettings.LeftMargin + _printSettings.CurrentXOffset, currY + _printSettings.FontOffset);
            AllocatedHeight += _printSettings.FontOffset;
            currY += _printSettings.FontOffset;
          }
          //draw the line that goes across the bottom
          e.Graphics.DrawLine(Pens.Black, _printSettings.LeftMargin, currY, _printSettings.LeftMargin + _printSettings.CurrentXOffset, currY);
        }
      }
    }

    /// <summary>
    /// Adds the column header to the top of the printed page and returns the appropriate offset to start printing the rest of the page
    /// </summary>
    /// <param name="fontOffset">The Height of the font</param>
    private int AddColumnHeaders(object sender, PrintPageEventArgs e, int fontOffset)
    {
      int headerWidth = 0;
      int y = (int)_printSettings.TopMargin;

      var currPage = from C
                     in PageColumns
                     where C.Value == _printSettings.currentPartialPage
                     select C;

      foreach (var v in currPage)
      {
        //draw vertical line to left side of text
        e.Graphics.DrawLine(Pens.Black, _printSettings.LeftMargin + headerWidth, y, _printSettings.LeftMargin + headerWidth, y + fontOffset);
        //draw text
        e.Graphics.DrawString(v.Key.name, printFont, Brushes.Black, new RectangleF(_printSettings.LeftMargin + headerWidth, y, v.Key.width, fontOffset));
        //omit left line as it will be drawn by next iteration
        headerWidth += v.Key.width;
      }

      //close box, first last vertical line
      e.Graphics.DrawLine(Pens.Black, _printSettings.LeftMargin + headerWidth, y, _printSettings.LeftMargin + headerWidth, y + fontOffset);
      //draw the line that goes across the top
      e.Graphics.DrawLine(Pens.Black, _printSettings.LeftMargin, y, _printSettings.LeftMargin + headerWidth, y);
      //draw the line that goes across the bottom
      e.Graphics.DrawLine(Pens.Black, _printSettings.LeftMargin, y + fontOffset, _printSettings.LeftMargin + headerWidth, y + fontOffset);
      return y + fontOffset;
    }

    /// <summary>
    /// Adds the page number at the bottom of the print document
    /// </summary>
    private void addPageNumber(object sender, PrintPageEventArgs e)
    {
      int centerLocation;
      string pageNumberString = "Page: " + ++_printSettings.PageNumber;

      Rectangle pageNumberLocation = new Rectangle();
      pageNumberLocation.Width = _printSettings.paperwidth;
      pageNumberLocation.Height = _printSettings.BottomMargin;

      centerLocation = (int)(_printSettings.paperwidth / 2) - (int)(e.Graphics.MeasureString(pageNumberString, new Font("Times New Roman", 12, FontStyle.Regular)).Width / 2);
      pageNumberLocation.Location = new Point(centerLocation, _printSettings.BottomMarginLocation);

      e.Graphics.DrawString(pageNumberString, new Font("Times New Roman", 12, FontStyle.Regular), Brushes.Black, pageNumberLocation, new StringFormat());
    }

    /// <summary>
    /// Adds the page header to the top of the page and adjusts margins as needed
    /// </summary>
    private void AddPageHeader(object sender, PrintPageEventArgs e)
    {
      if (HeaderImage == null) return;

      //construct rectangle for image to show in (letterhead)
      Rectangle headerbox;
      int ImagePad = 10; //padding between image and text
      int pagePad = 20; //padding between top of page and header

      if (_printSettings.PageNumber == 0)
      {
        headerbox = new Rectangle(_printSettings.LeftMargin, _printSettings.PageTop + pagePad, _printSettings.paperwidth - _printSettings.LeftMargin - (_printSettings.PageRightLocation - _printSettings.RightMargin), _printSettings.TopMargin + 35);

        //resize print area to accomidate
        _printSettings.TopMargin = _printSettings.PageTop + headerbox.Height + ImagePad + pagePad;
      }
      else //bounds get adjusted after first run, so now just use it unchanged (except padding added above
        headerbox = new Rectangle(_printSettings.LeftMargin, _printSettings.PageTop + pagePad, _printSettings.paperwidth - _printSettings.LeftMargin - (_printSettings.PageRightLocation - _printSettings.RightMargin), _printSettings.TopMargin - ImagePad - pagePad);

      //draw image in rectangle
      e.Graphics.DrawImage(HeaderImage, headerbox);
    }

    /// <summary>
    /// Returns the current work item to print
    /// </summary>
    /// <returns></returns>
    private object GetWorkItem()
    {
      //used to keep track of whick transaction is current for pages 2 -x
      object currWork = null;

      if (_printSettings.currentPartialPage == 1)
      {
        if (_toPrint.Count > 0)
        {
          currWork = _toPrint[0];
          _toPrint.Remove(currWork);
          inProgress.Add(currWork);
        }
      }
      else if (inProgress.Count > 0 && _printSettings.workItem < inProgress.Count)
      {
        currWork = inProgress[_printSettings.workItem++];
      }

      return currWork;
    }
    #endregion

    #region private helpers
    /// <summary>
    /// Loads the print queue
    /// </summary>
    private void QueuePrintLines()
    {
      object[] items = new object[ToPrint.Count];
      ToPrint.CopyTo(items);
      _toPrint = new List<object>(items);
      inProgress = new List<object>();
      QueueManyItems();
    }

    /// <summary>
    /// Loads queue appropriate for a list of transactions
    /// </summary>
    private void QueueManyItems()
    {
      PageColumns = new Dictionary<PrintColumn, int>();
      int NumberOfPageParts = 1;
      int PrintAreaWidth = 0;

      if (pageSettings.Landscape)
        PrintAreaWidth = (int)pageSettings.PrintableArea.Height - pageSettings.Margins.Left - (pageSettings.Margins.Right);
      else
        PrintAreaWidth = (int)pageSettings.PrintableArea.Width - pageSettings.Margins.Left - (pageSettings.Margins.Right);
      int AllocatedWidth = 0;

      foreach (Column c in printfrom.Columns.VisibleItems)
      {
        int widthLeft = PrintAreaWidth - AllocatedWidth;

        if (widthLeft > c.Width)
        {
          AllocatedWidth += c.Width;
        }
        else
        {
          NumberOfPageParts++;
          AllocatedWidth = c.Width;
        }

        if (c.ColumnItemAccessor != null)
        {

          PageColumns.Add(new PrintColumn()
          {
            name = c.Name,
            width = c.Width,
            value = c.ColumnItemAccessor
          }, NumberOfPageParts);
        }

        else if (c.ColumnItemAccessorWithParameter != null)
        {
          PageColumns.Add(new PrintColumn()
          {
            name = c.Name,
            width = c.Width,
            value2 = c.ColumnItemAccessorWithParameter,
            valueParameter = c.AccessorParameter
          }, NumberOfPageParts);
        }

      }
    }
    #endregion
  }
}
